#include "Terrain.hpp"

// Yeaah, could do better
#include "lut_array.inc"



void Terrain::init()
{
  initFirstPass();
  initSecondPass();
  initThirdPass();  
  initFinalPass();
  
  glGenQueries( 1, &m_queryTF);
  
  
  for (int i=0; i<GRID_DIM; ++i)
  for (int j=0; j<GRID_DIM; ++j)
  for (int k=0; k<GRID_DIM; ++k) 
  {
    ChunkInfo &chunk = m_chunkInfo[i][j][k];
    chunk.i = i; chunk.j = j; chunk.k = k;
    createChunk( chunk );
  }
}

void Terrain::createChunk(ChunkInfo &chunk)
{
  GLint queryResult;
  
  
  int start = - GRID_DIM / 2;
  chunk.x = kChunkSize * (start + chunk.i);
  chunk.y = kChunkSize * (start + chunk.j);
  chunk.z = kChunkSize * (start + chunk.k);
  chunk.id = -1;
  
  
  /// 1) Generate the density volume
  buildDensityVolume( chunk );  
    
  /// 2)
  glBeginQuery( GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queryTF);
    listTriangles( );
  glEndQuery( GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN );
  glGetQueryObjectiv( m_queryTF, GL_QUERY_RESULT, &queryResult);
  
  // Skip remaining passes if the chunk is empty
  if (queryResult == 0) {
    return;
  }
  //fprintf( stderr, "listTriangle output primitives : %d\n", queryResult);
  
  
  /// 3)
  glBeginQuery( GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, m_queryTF);
    generateVertices( chunk );
  glEndQuery( GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN );
  glGetQueryObjectiv( m_queryTF, GL_QUERY_RESULT, &queryResult);
  
  //fprintf( stderr, "generateVertices output primitives : %d\n", queryResult);
}


void Terrain::initFirstPass()
{  
  /// Quad to generate the 3D Volume slices
  // Quad positions + texcoords
  const float quad_data[] = { -1.0f, +1.0f,  0.0f, 1.0f,
                              -1.0f, -1.0f,  0.0f, 0.0f,
                              +1.0f, +1.0f,  1.0f, 1.0f,
                              +1.0f, -1.0f,  1.0f, 0.0f};
  
  // vao needed !
  m_quadVB.generate();
  m_quadVB.bind( aer::DeviceBuffer::ARRAY );
    m_quadVB.setData( quad_data, sizeof(quad_data));
  m_quadVB.unbind();
  
  
  /// Build Density Program
  m_buildDensityPrgm.generate();
    m_buildDensityPrgm.addShader( aer::Program::VERTEX_SHADER,   "1_BuildDensityVolume.VS");
    m_buildDensityPrgm.addShader( aer::Program::GEOMETRY_SHADER, "1_BuildDensityVolume.GS");
    m_buildDensityPrgm.addShader( aer::Program::FRAGMENT_SHADER, "1_BuildDensityVolume.FS");
  AER_CHECK( m_buildDensityPrgm.link() );
  
  CHECKGLERROR();
    
  /// Generate the Density Volume Texture
  glGenTextures( 1, &m_densityTex);
  glBindTexture( GL_TEXTURE_3D, m_densityTex);
    GLint filter = GL_LINEAR;//GL_NEAREST;
    glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, filter);
    glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, filter);
    glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    
    glTexImage3D( GL_TEXTURE_3D, 0, 
                                 GL_R32F, //
                                 kChunkDim+1, kChunkDim+1, kChunkDim+1,
                                 0, GL_RED, GL_FLOAT, NULL);
  glBindTexture( GL_TEXTURE_3D, 0u);
  
  CHECKGLERROR();
  
  /// Generate the Framebuffer
  glGenFramebuffers( 1, &m_densityRT);
  glBindFramebuffer( GL_FRAMEBUFFER, m_densityRT);
    glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, m_densityTex, 0);
  AER_CHECK( aer::RenderTarget::CheckStatus() );//
  glBindFramebuffer( GL_FRAMEBUFFER, 0u);
  
  
//--------
  //DEBUG
  m_dispDensityPrgm.generate();
    m_dispDensityPrgm.addShader( aer::Program::VERTEX_SHADER,   "DispDensityVolume.VS");
    m_dispDensityPrgm.addShader( aer::Program::FRAGMENT_SHADER, "DispDensityVolume.FS");
  AER_CHECK( m_dispDensityPrgm.link() );
//--------
  
  
  CHECKGLERROR();
}


void Terrain::buildDensityVolume(ChunkInfo &chunk)
{
  glDisable( GL_DEPTH_TEST );
  glDepthMask( GL_FALSE );
    
  
  // [COMMENT BOTH LINES TO SEE RESULT]
  glBindFramebuffer( GL_FRAMEBUFFER, m_densityRT);  
    glDrawBuffer( GL_COLOR_ATTACHMENT0 );  
  
    glViewport( 0, 0, kChunkDim+1, kChunkDim+1);
    
    m_buildDensityPrgm.bind();
    {
      m_buildDensityPrgm.setUniform( "uChunkPositionWS", chunk.x, chunk.y, chunk.z);
      m_buildDensityPrgm.setUniform( "uInvChunkDim", 1.0f / float(kChunkDim));//
      m_buildDensityPrgm.setUniform( "uChunkSize", kChunkSize);   
      
      m_quadVB.bind();
       
      glVertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, 0, (void*)(0));    
      glEnableVertexAttribArray( 0 );
        glDrawArraysInstanced( GL_TRIANGLE_STRIP, 0, 4, kChunkDim+1);
      glDisableVertexAttribArray( 0 );
      m_quadVB.unbind();
    }
    m_buildDensityPrgm.unbind();    
  glBindFramebuffer( GL_FRAMEBUFFER, 0u);
  
  glEnable( GL_DEPTH_TEST );
  glDepthMask( GL_TRUE );
  
  CHECKGLERROR();
}




void Terrain::initSecondPass()
{
  ///
  m_listTriangle.program.generate();
    m_listTriangle.program.addShader( aer::Program::VERTEX_SHADER,   "2_ListTriangle.VS");
    m_listTriangle.program.addShader( aer::Program::GEOMETRY_SHADER, "2_ListTriangle.GS");
    
    const GLchar* varyings[] = { "x6y6z6_e4e4e4" };
    glTransformFeedbackVaryings( m_listTriangle.program.getId(), AER_ARRAYSIZE(varyings), varyings, 
                                 GL_INTERLEAVED_ATTRIBS);  
  AER_CHECK( m_listTriangle.program.link() );
  
  
  
  /// Buffer used to generate the triangle list
  /// We use a 2D grid of points, instanced as 'depth' layers
  m_listTriangle.baseBuffer.generate();
  m_listTriangle.baseBuffer.bind();
  {
    const aer::U32 numElems = 2*kChunkDim*kChunkDim;
    float *data = new float[numElems];
    
    for (int j=0, index=0; j<kChunkDim; ++j)
    {
      for (int i=0; i<kChunkDim; ++i, index+=2)
      {
        data[index+0] = i;
        data[index+1] = j;
      }
    }
    m_listTriangle.baseBuffer.setData( data, numElems*sizeof(float), aer::DeviceBuffer::STATIC_DRAW); 
    delete [] data;
  }
  m_listTriangle.baseBuffer.unbind();
  
  
  /// Buffer containing the triangle list packed as unsigned int
  /// (3x6bits for voxel coordinates, 3x4bits for edges indices)
  m_listTriangle.buffer.generate();
  m_listTriangle.buffer.bind( aer::DeviceBuffer::ARRAY );
    // There is a maximum of 5 triangles per voxels in a chunk
    aer::U32 numElems = 5*kChunkDim*kChunkDim*kChunkDim;
    m_listTriangle.buffer.setData( NULL, numElems*sizeof(int), aer::DeviceBuffer::DYNAMIC_DRAW);
  m_listTriangle.buffer.unbind();  
  
  /// Transform feedback used to generate the triangle list
  glGenTransformFeedbacks( 1, &m_listTriangle.transformFeedback_id );  
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, m_listTriangle.transformFeedback_id);
    glBindBufferBase( GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_listTriangle.buffer.getId());
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, 0u);
  
  
  
  /// Setup texture buffers for LUT storing :
  /// The number of triangle per voxel case (256 bytes value in [0-5])
  glGenBuffers( 1, &m_listTriangle.tbo_caseToNumTri_id);
  /// The 0 to 5 trio of edge indices where lies the triangle vertices 
  /// (5*256 short value, stored as 3x4bits)
  glGenBuffers( 1, &m_listTriangle.tbo_edgeConnect_id);
  
  glBindBuffer( GL_TEXTURE_BUFFER, m_listTriangle.tbo_caseToNumTri_id);
  glBufferData( GL_TEXTURE_BUFFER, sizeof(s_caseToNumpolys), 
                                   s_caseToNumpolys, 
                                   GL_STATIC_DRAW);
  
  glBindBuffer( GL_TEXTURE_BUFFER, m_listTriangle.tbo_edgeConnect_id);
  glBufferData( GL_TEXTURE_BUFFER, sizeof(s_packedEdgesIndicesPerCase),
                                   s_packedEdgesIndicesPerCase, 
                                   GL_STATIC_DRAW);
  glBindBuffer( GL_TEXTURE_BUFFER, 0u);
  
  glGenTextures( 1, &m_listTriangle.tex_caseToNumTri_id);
  glGenTextures( 1, &m_listTriangle.tex_edgeConnect_id);
  
  glBindTexture( GL_TEXTURE_BUFFER, m_listTriangle.tex_caseToNumTri_id);
  glTexBuffer( GL_TEXTURE_BUFFER, GL_R8I, m_listTriangle.tbo_caseToNumTri_id);
  glBindTexture( GL_TEXTURE_BUFFER, m_listTriangle.tex_edgeConnect_id);
  glTexBuffer( GL_TEXTURE_BUFFER, GL_R16I, m_listTriangle.tbo_edgeConnect_id);
  glBindTexture( GL_TEXTURE_BUFFER, 0u);
  
  CHECKGLERROR();
}

void Terrain::listTriangles()
{
  glEnable( GL_RASTERIZER_DISCARD );
  
  
  m_listTriangle.program.bind();
  m_listTriangle.program.setUniform( "uDensityVolume",   0);
  m_listTriangle.program.setUniform( "uCaseToNumTri",    1);
  m_listTriangle.program.setUniform( "uEdgeConnectList", 2);
  m_listTriangle.program.setUniform( "uInvChunkDimMinusOne", 1.0f / (kChunkDim-1)); //  
  m_listTriangle.program.setUniform( "uTexelSize", aer::Vector2(1.f/(kChunkDim+1), 0.0f) );
  
  glActiveTexture( GL_TEXTURE0 + 0);  glBindTexture( GL_TEXTURE_3D, m_densityTex);
  glActiveTexture( GL_TEXTURE0 + 1);  glBindTexture( GL_TEXTURE_BUFFER, m_listTriangle.tex_caseToNumTri_id);
  glActiveTexture( GL_TEXTURE0 + 2);  glBindTexture( GL_TEXTURE_BUFFER, m_listTriangle.tex_edgeConnect_id);
  
  // transform feedback wheres VS/GS outputs will be redirected
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, m_listTriangle.transformFeedback_id);
  glBeginTransformFeedback( GL_POINTS );
  { 
    m_listTriangle.baseBuffer.bind();
      glEnableVertexAttribArray(0);
      glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 0, 0);  
    
    unsigned int voxelsPerSlice = kChunkDim*kChunkDim;
    //glDrawArrays( GL_POINTS, 0, voxelsPerSlice);
    glDrawArraysInstanced( GL_POINTS, 0, voxelsPerSlice, kChunkDim);
    
    glDisableVertexAttribArray(0);
    m_listTriangle.baseBuffer.unbind();  
  }
  glEndTransformFeedback();
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, 0u);
  
  glActiveTexture( GL_TEXTURE0 + 2);  glBindTexture( GL_TEXTURE_BUFFER, 0u);
  glActiveTexture( GL_TEXTURE0 + 1);  glBindTexture( GL_TEXTURE_BUFFER, 0u);
  glActiveTexture( GL_TEXTURE0 + 0);  glBindTexture( GL_TEXTURE_3D, 0u);
  m_listTriangle.program.unbind();  
  
  
  glDisable( GL_RASTERIZER_DISCARD );
  
  CHECKGLERROR();
}




void Terrain::initThirdPass()
{
  ///
  m_genVerticesPrgm.generate();
    m_genVerticesPrgm.addShader( aer::Program::VERTEX_SHADER,   "3_GenVertices.VS");
    m_genVerticesPrgm.addShader( aer::Program::GEOMETRY_SHADER, "3_GenVertices.GS");
    
    const GLchar* varyings[] = { "outPositionWS", "outNormalWS" };
    glTransformFeedbackVaryings( m_genVerticesPrgm.getId(), 
                                 AER_ARRAYSIZE(varyings), varyings, 
                                 GL_INTERLEAVED_ATTRIBS);  
  AER_CHECK( m_genVerticesPrgm.link() );
    
  
  
  glGenTransformFeedbacks( kBufferBatchSize, m_chunkTF );  
  m_numFreeBuffer = kBufferBatchSize;
  
  const aer::U32 bufferSize = (128*1024) * sizeof(float);  //
  for (aer::U32 i=0u; i<kBufferBatchSize; ++i)
  {
    aer::DeviceBuffer &buffer = m_chunkBuffer[i];
    m_freeBuffer[i] = i;
    
    buffer.generate();
    buffer.bind( aer::DeviceBuffer::ARRAY );
    buffer.setData( NULL, bufferSize, aer::DeviceBuffer::DYNAMIC_DRAW);
  
    glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, m_chunkTF[i]);
    glBindBufferBase( GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer.getId());
  }
  m_chunkBuffer[0].unbind( );
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, 0u); 

  
  CHECKGLERROR();
}

void Terrain::generateVertices(ChunkInfo &chunk)
{
  glEnable( GL_RASTERIZER_DISCARD );
  
  
  m_genVerticesPrgm.bind();
  m_genVerticesPrgm.setUniform( "uDensityVolume", 0);//
  m_genVerticesPrgm.setUniform( "uChunkSize", kChunkSize);//
  m_genVerticesPrgm.setUniform( "uInvChunkDim", 1.0f/float(kChunkDim));//
  m_genVerticesPrgm.setUniform( "uChunkPositionWS", chunk.x, chunk.y, chunk.z);
  
  glBindTexture( GL_TEXTURE_3D, m_densityTex);//
  
  
  if (m_numFreeBuffer > 0) 
  {
    chunk.id = m_freeBuffer[kBufferBatchSize - m_numFreeBuffer];
    --m_numFreeBuffer;
  } 
  else 
  {
    fprintf( stderr, "Warning: no more free vbo\n");
    chunk.id = -1;
    return;
  }
  
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, m_chunkTF[chunk.id]);
  glBeginTransformFeedback( GL_POINTS );
  { 
    m_listTriangle.buffer.bind();
    
    // Values are stored as packed integer [IMPORTANT]
    glVertexAttribIPointer( 0, 1, GL_INT, 0, NULL);
    
    glEnableVertexAttribArray(0);
      glDrawTransformFeedback( GL_POINTS, m_listTriangle.transformFeedback_id);    
    glDisableVertexAttribArray(0);
    m_listTriangle.buffer.unbind();  
  }
  glEndTransformFeedback();
  glBindTransformFeedback( GL_TRANSFORM_FEEDBACK, 0u);
  
  glBindTexture( GL_TEXTURE_3D, 0u);
  m_genVerticesPrgm.unbind();  
  
  
  glDisable( GL_RASTERIZER_DISCARD );
  
  CHECKGLERROR();  
}





void Terrain::initFinalPass()
{
  m_renderChunkPrgm.generate();
    m_renderChunkPrgm.addShader( aer::Program::VERTEX_SHADER,   "PassThrough.VS");
    m_renderChunkPrgm.addShader( aer::Program::FRAGMENT_SHADER, "PassThrough.FS");
  AER_CHECK( m_renderChunkPrgm.link() );
  
  m_cubeMesh.init( 1.0f );
  
  CHECKGLERROR();
}


void Terrain::render(aer::Camera *camera)
{
#if 1

  m_renderChunkPrgm.bind();
  {
    const aer::Matrix4x4 &viewProj = camera->getViewProjectionMatrix();
    
    m_renderChunkPrgm.setUniform( "uModelViewProjMatrix", viewProj);
    m_renderChunkPrgm.setUniform( "uColor", 0.0f, 0.2f, 1.0f);


    for(int i=0;i<GRID_DIM;++i) for(int j=0;j<GRID_DIM;++j) for(int k=0;k<GRID_DIM;++k)
    {
      ChunkInfo &chunk = m_chunkInfo[i][j][k];
      
      if (chunk.id < 0) {
        continue;
      }
      
      aer::DeviceBuffer &buffer = m_chunkBuffer[ chunk.id ];
      buffer.bind();
        const aer::U32 stride = 6*sizeof(float);
        glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, stride, 0);                          // Position
        glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3*sizeof(float))); // Normal
        
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
          glDrawTransformFeedback( GL_TRIANGLES, m_chunkTF[chunk.id]);
        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
      buffer.unbind();
    }

    /*
    aer::Matrix4x4 model;
    m_renderChunkPrgm.setUniform( "uModelViewProjMatrix", viewProj*model);
    m_renderChunkPrgm.setUniform( "uColor", 1.0f, 1.0f, 1.0f);
    m_cubeMesh.draw();
    */
  }
  m_renderChunkPrgm.unbind();

#else
  //DEBUG
  
  m_dispDensityPrgm.bind();  
    m_dispDensityPrgm.setUniform( "uModelViewProjMatrix", camera->getViewProjectionMatrix());
    m_dispDensityPrgm.setUniform( "uDensityVolume", 0);
    
    glActiveTexture( GL_TEXTURE0 );
    glBindTexture( GL_TEXTURE_3D, m_densityTex);
    
    m_quadVB.bind();       
    glVertexAttribPointer( 0, 4, GL_FLOAT, GL_FALSE, 0, (void*)(0));
    glEnableVertexAttribArray( 0 );
      glDrawArraysInstanced( GL_TRIANGLE_STRIP, 0, 4, kChunkDim+1);
    glDisableVertexAttribArray( 0 );
    m_quadVB.unbind();
    
  m_dispDensityPrgm.unbind();
  
#endif
  
  CHECKGLERROR();
}
